package apiroute

import (
	"fmt"
	"git.sr.ht/~mil/mobroute/src/csa"
	"git.sr.ht/~mil/mobroute/src/db"
	"git.sr.ht/~mil/mobroute/src/utilfuncs"
	"git.sr.ht/~mil/mobsql"
	"time"
)

// CommandRoute performs the routing logic, extracting from the DB the required
// data for CSA, running the CSA algorithm, going back to the DB to pull a few
// extra fields for presentation, and finally formatting the data into route
// legs for easy consumption:
//
// Essentially a 4 step process:
//
//  1. CSA Inputs Dataextract:
//     Extracts necessary data from the SQLite database, 4 inputs
//     extracted: connectionss, transfers, walksfrom, and walksto
//
//  2. CSA Execution:
//     Runs the CSA algorithm (returns []*db.Connection)
//
//  3. CSA Outputs Verbosity Dataextract:
//     Result of CSA algorithm is fitered []*db.Connection, which is a minimal
//     datastructure with just information needed for CSA - so in this
//     step we go back to the database to extract []*db.ConnectionVerbose
//     for the result of the CSA alg's connections.
//
//  4. Leg Formatting:
//     Formatting the []*db.ConnectionVerbose CSA results into a nice pretty
//     format showing each part of the trip:
//     - Leg for walking to input stop
//     - (n) trips (with each []*db.Connection nested within)
//     - (n) transfers between each trip
//     - Leg for walking from output stop to destination
func CommandRoute(paramsGiven *RouteParams) (*RouteResponse, error) {
	var (
		params           = paramsValueify(paramsGiven)
		timeYYYYMMDD     = utilfuncs.DayYYYYMMDD(params.Time)
		timeSecondsFrom  = utilfuncs.DaySeconds(params.Time)
		timeSecondsTo    = utilfuncs.DaySeconds(params.Time) + *params.MaxTripSeconds
		maxKmWalkSeconds = walkKmInSecondsGivenKmHr(*params.MaxWalkSeconds, *params.WalkspeedKmHr)
		timer            = Timer{lastTime: time.Now()}
		diagnosticsPerf  = DiagnosticsPerf{}

		err                      error
		dbConnection             *db.DBConnection
		memConns                 *[]db.Connection
		memConnsverbose          *[]db.ConnectionVerbose
		memTransfers             *[]db.Transfer
		memWalksfrom, memWalksto *[]db.StopWalk
		csaConns                 []*db.Connection
	)

	// (1) Mobsql load/compute initialize
	if dbConnection, err = db.Initialize(params.Filter, params.Time, true, true, false); err != nil {
		return nil, fmt.Errorf("Failed to initialize db connection: %v", err)
	} else {
		diagnosticsPerf.PerfSLoadcompute = timer.Snapshot()
	}

	// (2) Sanity checks
	if err = routeRequestSanityChecks(dbConnection, &params); err != nil {
		return nil, fmt.Errorf("Route request failed sanity check: %v", err)
	}

	// (3.1) Memload connections
	if memConns, err = dbConnection.LoadConnections(timeYYYYMMDD, timeSecondsFrom, timeSecondsTo); err != nil {
		return nil, fmt.Errorf("Failed to load connections from DB: %v", err)
	} else {
		diagnosticsPerf.PerfSMemloadConns = timer.Snapshot()
		diagnosticsPerf.PerfNMemConns = len(*memConns)
	}

	// (3.2) Memload transfers
	if memTransfers, err = dbConnection.LoadTransfers(
		*params.MinTransferSeconds, *params.MaxTransferSeconds,
		*params.WalkspeedKmHr,
		*params.TransfersTable, *params.TransfersGenerated,
	); err != nil {
		return nil, fmt.Errorf("Failed to load transfers from DB: %v", err)
	} else {
		diagnosticsPerf.PerfSMemloadTransfers = timer.Snapshot()
		diagnosticsPerf.PerfNMemTransfers = len(*memTransfers)
	}

	// (3.3) Memload walksfrom
	if memWalksfrom, err = dbConnection.LoadStopWalks(*params.From, maxKmWalkSeconds); err != nil {
		return nil, fmt.Errorf("Failed to load walksfrom from DB: %v", err)
	} else if nonNilWalksN := countNonNilWalks(memWalksfrom); nonNilWalksN == 0 {
		return nil, fmt.Errorf("Failed to determined >0 stopwalks to from dest: %v within max walk km: %0.2f", *params.From, maxKmWalkSeconds)
	} else {
		diagnosticsPerf.PerfSMemloadWalksfrom = timer.Snapshot()
		diagnosticsPerf.PerfNMemWalksfrom = len(*memWalksfrom)
		diagnosticsPerf.PerfNMemWalksfromNonnil = nonNilWalksN
	}

	// (3.4) Memload walksto
	if memWalksto, err = dbConnection.LoadStopWalks(*params.To, maxKmWalkSeconds); err != nil {
		return nil, fmt.Errorf("Failed to load walksto from DB: %v", err)
	} else if nonNilWalksN := countNonNilWalks(memWalksto); nonNilWalksN == 0 {
		return nil, fmt.Errorf("Failed to determined >0 stopwalks to to dest: %v within max walk km: %0.2f", *params.To, maxKmWalkSeconds)
	} else {
		diagnosticsPerf.PerfSMemloadWalksto = timer.Snapshot()
		diagnosticsPerf.PerfNMemWalksto = len(*memWalksto)
		diagnosticsPerf.PerfNMemWalkstoNonnil = nonNilWalksN
	}

	// (4) Run CSA
	if csaConns, err = csa.ExecuteCSA( // (2)
		memConns,
		memTransfers,
		walksToMapTimeSecs(memWalksfrom, *params.WalkspeedKmHr),
		walksToMapTimeSecs(memWalksto, *params.WalkspeedKmHr),
		timeSecondsFrom,
		*params.MinTransferSeconds,
		*params.MaxTransferSeconds,
		*params.MaxWalkSeconds,
		*params.MaxNTransfers,
	); err != nil {
		return nil, fmt.Errorf("Failed to run CSA: %v", err)
	} else {
		diagnosticsPerf.PerfSRoute = timer.Snapshot()
	}

	// (5)  Verbose connections
	if memConnsverbose, err = dbConnection.LoadConnectionsVerbose( // (3)
		csa.ConnIDs(csaConns),
	); err != nil {
		return nil, err
	} else {
		diagnosticsPerf.PerfSMemloadConnsverb = timer.Snapshot()
	}

	// (6)
	return formatRouteResponse(&diagnosticsPerf, memConnsverbose, memTransfers, &params), nil
}

func routeRequestSanityChecks(dbConnection *db.DBConnection, dbParams *RouteParams) error {
	if err := dbConnection.QueryBoundsMatch(*dbParams.From); err != nil {
		return fmt.Errorf("From %v failed to match any bounds: %v", dbParams.From, err)
	} else if err := dbConnection.QueryBoundsMatch(*dbParams.To); err != nil {
		return fmt.Errorf("To %v failed to match any bounds: %v", dbParams.To, err)
	} else if *dbParams.MaxTransferSeconds < *dbParams.MinTransferSeconds {
		return fmt.Errorf(
			"Failed condition: %d max_transfer_seconds < %d min_transfer_seconds",
			*dbParams.MaxTransferSeconds, *dbParams.MinTransferSeconds,
		)
	} else {
		return nil
	}
}

func paramsValueify(b *RouteParams) RouteParams {
	return RouteParams{
		CacheDir:           nil,
		Filter:             coalesce(b.Filter, &mobsql.SourcesetFilter{Mdbid: []int{516}}),
		From:               coalesce(b.From, &utilfuncs.Coord{Lat: 40.70940, Lon: -74.00537}),
		To:                 coalesce(b.To, &utilfuncs.Coord{Lat: 40.72879, Lon: -73.95215}),
		Time:               coalesce(b.Time, ptr(time.Now())),
		MaxWalkSeconds:     coalesce(b.MaxWalkSeconds, ptr(uint(1200))),
		MaxTripSeconds:     coalesce(b.MaxTripSeconds, ptr(uint(14400))),
		MinTransferSeconds: coalesce(b.MinTransferSeconds, ptr(uint(180))),
		MaxTransferSeconds: coalesce(b.MaxTransferSeconds, ptr(uint(2400))),
		WalkspeedKmHr:      coalesce(b.WalkspeedKmHr, ptr(3.5)),
		TransfersTable:     coalesce(b.TransfersTable, ptr(true)),
		TransfersGenerated: coalesce(b.TransfersGenerated, ptr(false)),
		DebugFormatting:    coalesce(b.DebugFormatting, ptr(false)),
		MaxNTransfers:      coalesce(b.MaxNTransfers, ptr(uint(5))),
		OutputFormats: coalesce(b.OutputFormats, ptr([]string{
			"route_legs", "diagnostics_route", "diagnostics_perf", "geojson", "mapurl",
		})),
	}
}
